/**
  * @(#)Settings.java
  *
  * Implement the application settings storage through Java properties files.
  *
  * @author Course Scheduler Team
  * 
  * @license GNU General Public License version 3 (GPLv3)
  *
  * This file is part of Course Scheduler, an open source, cross platform
  * course scheduling tool, configurable for most universities.
  *
  * Copyright (C) 2010-2012 Course Scheduler Team
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
  * the Free Software Foundation, either version 3 of the License, or
  * (at your option) any later version.
  *
  * This program is distributed in the hope that it will be useful,
  * but WITHOUT ANY WARRANTY; without even the implied warranty of
  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  * GNU General Public License for more details.
  *
  * You should have received a copy of the GNU General Public License
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  * 
  */
package com.pollicitus.scheduler.core;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;


/**
 * Implement the application settings through Java properties files
 * 
 * @author Course Scheduler Team
 *
 */
public class Settings {
	
	/**
	 * The default configuration directory relative to the current working directory. All
	 * relative configuration file references will be considered as relative to this directory.
	 * Absolute configuration file references will not be impacted by this value.
	 * <p>
	 * Value: {@value}
	 */
	public static final String DEFAULT_CONFIG_DIRECTORY = "conf/";
	
	/**
	 * The default base configuration file which will be used to pre-populate the properties 
	 * containers prior to loading the namespace properies. This is used to provide default
	 * values for a given property in every namespace.
	 * <p>
	 * Value: {@value}
	 */
	public static final String DEFAULT_BASE_CONFIG = "base.properties";
	
	/**
	 * The default location of the primary configuration file. The primary configuration file
	 * contains the basic configuration properties, but most importantly defines the other 
	 * namespaces that should loaded from different configuration files at application startup.
	 * <p>
	 * Value: {@value}
	 */
	public static final String DEFAULT_PRIMARY_CONFIG = "scheduler.properties";
	
	/**
	 * The character which is used to separate the property namespace from the property name.
	 * <p>
	 * Value: {@value}
	 */
	public static final String NAMESPACE_SEPARATOR = "#";
	
	/**
	 * Property namespace for base application configurations. This namespace is loaded automatically
	 * at initialization of the Settings class from the primary configuration file. 
	 * <p>
	 * Value: {@value}
	 */
	public static final String DEFAULT_NAMESPACE = "scheduler";
	
	/**
	 * Properties namespace for JRE system properties. This namespace is a placeholder which is used to
	 * retrieve properties from the System object via the System.getPropery method. This provides
	 * a consistent interface for retrieving application and system properties as well as allows for
	 * property substitution using System properties. 
	 * <p>
	 * Value: {@value}
	 */
	public static final String SYSTEM_NAMESPACE = "system";
	
	/**
	 * Properties namespace for accessing environment variables via the same interface as application
	 * properties. This also allows for including environment variables in property substitutions.
	 * <p>
	 * Value: {@value}
	 */
	public static final String ENVIRONMENT_NAMESPACE = "environment";
	
	/**
	 * Properties namespace for accessing the command line flags and arguments via the same interface
	 * as application properties. As with the environment and system variables, this allos including command
	 * line flag values and program parameters in property substitutions.
	 * <p>
	 * Value: {@value}
	 */
	public static final String PARAMETER_NAMESPACE = "parameter";
	
	/**
	 * The property in the primary configuration files used to find other namespaces which should be loaded
	 * during application initialization. This property should consist of a comma separated list of qualified 
	 * namespaces, for which each must have a corresponding property indicating the file from which the namespace
	 * should be loaded.
	 * <p>
	 * Value: {@value}
	 */
	private static final String EXTENSION_NAMESPACES = "namespaces";
	
	/**
	 * The name of the boolean property in each configuration file which indicates whether the configuration file
	 * should automatically be flushed back to disk upon normal application termination. If this property is set to 
	 * true for a given namespace, any unsaved changes will automatically be saved to the source configuration file.
	 * If this property is set to false for a given namespace, any unsaved changes will be discarded when the application
	 * terminates.
	 * <p>
	 * Value: {@value} 
	 */
	private static final String SETTINGS_AUTOFLUSH = "autoflush";
	
	/**
	 * The namespace prefix which prepends the namespace in the namespace declaration. The namespaces declared
	 * in the property specified by PROPERTY_NAMESPACE should be prepended by this string in order for the application
	 * to properly separate the declaration from the namespace proper. 
	 * <p>
	 * Example: The namespace declared by {@value}example will be defined by a property named {@value}example
	 * 			in this namespace which indicates the source file for the properties which will be loaded into
	 * 			the Settings class as the example namespace.
	 * <p>
	 * Value: {@value}
	 */
	private static final String NAMESPACE_PROPERTY_PREFIX = "namespace.";
	
	/**
	 * The string which delimits between two values in a multi-valued property. This string is used in particular
	 * by the getPropertyArray logic which splits the value of the property.
	 * <p>
	 * Value: {@value}
	 */
	protected static final String VALUE_DELIMITER = ",";
	
	/**
	 * The position of the namespace identifier in a fully-qualified property name. This indicates where in
	 * the property name array the namespace will be found when it is split based on the namespace separator.
	 * <p>
	 * Value: {@value}
	 */
	protected static final int NAMESPACE_POSITION = 0;
	
	/**
	 * The position of the property name in a fully-qualified property name. This indicats where in the 
	 * property name array the relative property name will be found when it is split based on the namespace
	 * separator.
	 * <p>
	 * Value: {@value}
	 */
	protected static final int SETTING_NAME_POSITION = 1;
	
	/**
	 * When a property includes substitutions of the values of other properties, it is possible that a 
	 * series of properties have a circular dependency. Alternatively, it is possible that dependency
	 * tree for a given property is inordinately large. Infinite loops and excessive memory utilization
	 * are prevented by limiting the total number of substitutions which are allowed in a single property
	 * retrieval.
	 * <p>
	 * Value: {@value}
	 */
	protected static final int SUBSTITUTION_COUNT_LIMIT = 20;
	
	/**
	 * During property value substitution, the innermost property variable must be selected for substitution
	 * first in order to insure that all properties are properly replaced. This regular expression selects the 
	 * first, innermost, standalone property name within the string.
	 * <p>
	 * Value: {@value}
	 */
	protected static final String SUBSTITUTION_PLACEHOLDER = "(\\$\\{[\\w\\.\\-#]+[^\\$\\(.*\\}]*\\})";
	
	/**
	 * During property value substitution, it is necessary to remove the variable declaration bookends from
	 * the variable selected by the SUBSTITUTION_PLACEHOLDER in order to have the pure property name for use
	 * in retrieving the replacement value. This regular expression selects only the property name portion of
	 * the property placeholder/
	 * <p>
	 * Value: {@value}
	 */
	protected static final String SUBSTITUTION_BOOKENDS = "([\\w.\\-#]+)";
		
	/**
	 * The base configuration directory. All relative configuration file references are assumed to be relative
	 * to this directory. Absolute configuration file references are not impacted by this member when loading
	 * or saving configuration files.
	 */
	private String baseDirectory;
	
	/**
	 * The {@link}Parameters object containing the flags and parameters specified when the application was 
	 * invoked. These values can be accessed directly through a Settings instance by using the PARAMETER_NAMESPACE.
	 */
	private Parameters parms;
	
	/**
	 * The {@link} Properties object containing the properties in the default configuration file. These properties
	 * are used as a baseline for every namespace loaded throughout the lifetime of the application. This configuration
	 * file represents a simple method of providing default values for properties for every namespace.
	 */
	private Properties baseProps;				
	
	/**
	 * The set of {@link} Properties objects corresponding to each loaded namespace. Each Properties object in
	 * this set must have a corresponding entry in the files map. 
	 */
	private Map<String, Properties> properties;
	
	/**
	 * The list of files that correspond to each loaded namespace. Each Properties object in the properties 
	 * map must have a corresponding entry in this map in order to successfully refresh the properties from 
	 * disk or flush the contents back to disk.
	 */
	private Map<String, String> files;			//map of files based on namespace
	
		/**
	 * Build the application primary instance based on the command line
	 * arguments. This method is called by the application entry point 
	 * during initialization.
	 * 
	 * @param args the command line parameters found by the application
	 * @return the primary settings instance
	 * @throws IOException problem reading configFile file
	 * @throws FileNotFoundException configFile file not found 
	 */
	public static Settings getInstance(Parameters args) throws FileNotFoundException, IOException{
		String baseConfigFile = args.getFlagValue(Parameters.BASE_CONFIG_FILE);
		String primaryConfigFile = args.getFlagValue(Parameters.PRIMARY_CONFIG_FILE);
		String configDirectory = args.getFlagValue(Parameters.CONFIG_DIRECTORY);
		return new Settings(args, configDirectory, baseConfigFile, primaryConfigFile);
	}
	
	/**
	 * Create a new Settings instance.
	 * 
	 * This should only be used by the Settings class. Subclasses should
	 * use the Settings(Settings) constructor to insure that application
	 * properties are properly reflected in all instances.
	 */
	private Settings(){
		properties = new HashMap<String, Properties>();
		files = new HashMap<String, String>();
		baseProps = new Properties();
	}
	
	/**
	 * Create a new Settings instance using the base configuration file specified.
	 * This should only be used by the getInstance method when constructing the 
	 * primary application settings object.
	 *
	 * @param properties the input parameters to the application when invoked
	 * @oaram configDirectory the root config directory for relative config paths
	 * @param baseConf the base configuration file
	 * @param primaryConf the primary configuration file
	 * @throws IOException problem reading configFile file
	 * @throws FileNotFoundException configFile file not found
	 */
	private Settings(Parameters parms, String configDirectory, String baseConf, String primaryConf) throws FileNotFoundException, IOException{
		this();
		
		//save the base directory for the application configuration
		baseDirectory = configDirectory;
		
		//store the command line parameters and flags
		this.parms = parms;
		
		//load the base configuration file
		baseProps.load(new FileInputStream(adjustPath(baseConf)));
		
		//load the primary configuration file
		load(DEFAULT_NAMESPACE, primaryConf);
		
		//load the remaining configFile files based on the primary config file
		String[] nameSpaces = getPropertyArray(DEFAULT_NAMESPACE + 
				NAMESPACE_SEPARATOR + EXTENSION_NAMESPACES);
		
		for(String extendedNameSpace: nameSpaces){
			String fileName = getProperty(DEFAULT_NAMESPACE + 
				NAMESPACE_SEPARATOR + extendedNameSpace);
			
			//remove the starting namespace property descriptor
			String nameSpace = extendedNameSpace.replaceAll(NAMESPACE_PROPERTY_PREFIX, "");
			
			//load the namespace from the filename
			load(nameSpace, fileName);
		}
	}
	
	/**
	 * Create a new settings instance which is based of off a previous settings
	 * instance. This should be used by subclasses to provide the existing settings
	 * from the primary settings object.
	 * 
	 * @param settings a "base on" settings object, should always be the application
	 * primary settings instance
	 */
	protected Settings(Settings settings){
		properties = settings.properties;
		files = settings.files;
		baseDirectory = settings.baseDirectory;
		parms = settings.parms;
		baseProps = settings.baseProps;
	}
	
	/**
	 * Return an array containing the namespaces which have been loaded into 
	 * this settings object. 
	 * 
	 * @return String[] of loaded namespaces
	 */
	public String[] getNamespaces(){
		//namespaces are in the keySet of the Map
		return properties.keySet().toArray(
				new String[properties.size()]
		);
	}
	
	/**
	 * Return the setting string for the specified property. Assumes that
	 * the property name includes the property namespace.
	 * 
	 * All other getProperty methods funnel through this except for 
	 * getProperty(namespace, settingName). Any subclass which needs
	 * to add a namespace to the property name should do so by 
	 * overriding this method (or the getProperty(namespace, settingName)
	 * method).
	 * 
	 * @param propertyName property name string NAMESPACE#NAMEVALUE
	 * @return the value of the application setting property
	 */
	public String getProperty(String propertyName){
		String[] propertyNameParts = propertyName.split(NAMESPACE_SEPARATOR);
		
		return getProperty(
				propertyNameParts[NAMESPACE_POSITION],
				propertyNameParts[SETTING_NAME_POSITION]
		);
	}
	
	/**
	 * Set a string property from the string value. Assumes that
	 * the property name includes the property namespace.
	 * 
	 * @param propertyName property name String NAMESPACE#NAMEVALUE
	 * @param propertyValue the new value of the application setting
	 */
	public void setProperty(String propertyName, String propertyValue){
		String[] propertyNameParts = propertyName.split(NAMESPACE_SEPARATOR);
		
		setProperty(
				propertyNameParts[NAMESPACE_POSITION],
				propertyNameParts[SETTING_NAME_POSITION],
				propertyValue
		);
	}
	
	/**
	 * Return a multi-value property as an array of values. Assumes that
	 * the property name includes the property namespace.
	 * 
	 * @param propertyName property name string NAMESPACE#NAMEVALUE
	 * @return the array of values stored in the setting property
	 */
	public String[] getPropertyArray(String propertyName){
		String property = getProperty(propertyName);
		
		//return an empty array if the property is the empty string, otherwise split it
		if(property.compareTo("") == 0) {
			return new String[]{};
		}else {
			return property.split(VALUE_DELIMITER);
		}
	}
	
	/**
	 * Set a multi-value property from an array of values. Assumes that
	 * the property name includes the property namespace.
	 * 
	 * @param propertyName property name String NAMESPACE#NAMEVALUE
	 * @param propertyValues a string array representing the new property value
	 */
	public void setPropertyArray(String propertyName, String[] propertyValues){
		if(propertyValues.length == 0){
			setProperty(propertyName, new String());
		}else{
			String value = propertyValues[0];
			
			//insure that all values are added properly and
			//that extraneous delimiters are not included
			int pos = 1;
			while(pos < propertyValues.length){
				value = VALUE_DELIMITER + propertyValues[pos];
			}
			
			//set the new property
			setProperty(propertyName, value);
		}
	}
	
	/**
	 * Return a numeric property as an int. Assumes that the property
	 * name includes the property namespace.
	 * 
	 * @param propertyName property name string NAMESPACE#NAMEVALUE
	 * @return the integer value of the setting property
	 */
	public int getPropertyInt(String propertyName){
		return Integer.parseInt(getProperty(propertyName));
	}
	
	/**
	 * Set a numeric property from an int. Assumes that the property
	 * name includes the property namespace.
	 * 
	 * @param propertyName property name String NAMESPACE#NAMEVALUE
	 * @param propertyValue an Integer representing the new property value
	 */
	public void setPropertyInt(String propertyName, Integer propertyValue){
		setProperty(propertyName, propertyValue.toString());
	}
	
	/**
	 * Return a numeric property as a double. Assumes that the property
	 * name includes the property namespace.
	 * 
	 * @param propertyName property name string NAMESPACE#NAMEVALUE
	 * @return the double value of the setting property
	 */
	public double getPropertyDouble(String propertyName){
		return Double.parseDouble(getProperty(propertyName));
	}
	
	/**
	 * Set a numeric propety from a double. Assumes that the property
	 * name includes the property namespace.
	 * 
	 * @param propertyName property name String NAMESPACE#NAMEVALUE
	 * @param propertyValue a Double representing the new property value
	 */
	public void setPropertyDouble(String propertyName, Double propertyValue){
		setProperty(propertyName, propertyValue.toString()); 
	}
	
	/**
	 * Return a true/false property as a boolean. Assumes that the 
	 * property name includes the property namespace.
	 * 
	 * @param propertyName property name string NAMESPACE#NAMEVALUE
	 * @return the boolean value of the setting property
	 */
	public boolean getPropertyBoolean(String propertyName){
		return Boolean.valueOf(getProperty(propertyName));
	}
	
	/**
	 * Set the specified property from a boolean. Assumes that the
	 * property name includes the property namespace
	 * 
	 * @param propertyName property name String NAMESPACE#NAMEVALUE
	 * @param propertyValue a Boolean representing the new property value
	 */
	public void setPropertyBoolean(String propertyName, Boolean propertyValue){
		setProperty(propertyName, propertyValue.toString());
	}

	/**
	 * Return the setting string for the specified property. The property
	 * is specified based on namespace and settingName.
	 * 
	 * @param namespace the property namespace where the property is located
	 * @param settingName the name of the property within it's namespace
	 * @return the value of the property (as identified by the namespace and settingName)
	 */
	private String getProperty(String namespace, String settingName) {
		
		// Temporary result prior to performing variable expansion
		String result;
		
		// Perform an unsubstituted property retrieval
		result = retrieveProperty(namespace, settingName);
		
		// Find and replace all instances of string substitution variables
		result = substituteProperties(namespace, result);
		
		return result;
	}
	
	/**
	 * Set the specified property to the specified value. The property 
	 * is specified based on the namespace and settingName
	 * 
	 * @param namespace the property namespace where the property is located
	 * @param settingName the name of the property within it's namespace
	 * @param settingValue the new value of the property (as identified by namespace and settingName)
	 */
	private void setProperty(String namespace, String settingName, String settingValue){
		properties.get(namespace).put(settingName, settingValue);
	}
	
	/**
	 * Return the setting string for the specified property. The property
	 * is specified based on namespace and settingName. No property substitution is performed
	 * 
	 * @param namespace the property namespace where the property is located
	 * @param settingName the name of the property within it's namespace
	 * @return the value of the property (as identified by the namespace and settingName)
	 */
	private String retrieveProperty(String namespace, String settingName){
		String result;
		
		if(namespace.compareTo(SYSTEM_NAMESPACE) == 0){
			//for properties which are under the system namespace, return the system property
			result = System.getProperty(settingName);
		}else if (namespace.compareTo(ENVIRONMENT_NAMESPACE) == 0){
			//for properties which are under the environment namespace, return the environment property
			result = System.getenv(settingName);
		}else if(namespace.compareTo(PARAMETER_NAMESPACE) == 0){
			try{
				//access a positional argument to the application, indicated by a numeric property name
				Integer index = Integer.parseInt(settingName);
				result = parms.getParameters(index);
			}catch(NumberFormatException e){
				//access a flag invoked with the application, indicated by the flag name
				result = parms.getFlagValue(settingName);
			}
		}else{
			//otherwise, return the custom property
			result = properties.get(namespace).getProperty(settingName);
		}
		
		return (result != null) ? result.trim() : null;
	}
	
	/**
	 * Find all occurrences of unsubstituted variables in the string and replace them
	 * 
	 * @param namespace the namespace to use for relative property references
	 * @param input the string on which to perform the property substitution
	 * @return the substituted version of the string
	 */
	private String substituteProperties(String namespace, String input){
		int replacementCount = 0;
		String result;
		Pattern placeholder = Pattern.compile(SUBSTITUTION_PLACEHOLDER);
		Pattern bookends = Pattern.compile(SUBSTITUTION_BOOKENDS);
		
		// Ensure that we don't attempt to process null input, propogate nulls instead
		if (input == null){
			return null;
		}else{
			result = new String(input);
		}
		
		//ANALYZE investigate the opportunity to perform substitution caching - this will require the ability to reliably perform some form of cache invalidation based on when properties are updated
		
		// As long as there are un-substituted properties (up to the substitution limit)
		Matcher placeholderMatcher = placeholder.matcher(result);
		while(placeholderMatcher.find() && replacementCount++ < SUBSTITUTION_COUNT_LIMIT){
			
			// Make sure we only match the first, inner most variable placeholder and then 
			// remove the variable 
			String variablePlaceHolder = placeholderMatcher.group();
			Matcher bookendMatcher = bookends.matcher(variablePlaceHolder);
			
			// If the variable is empty, return the original input (should not happen but we need to call
			//		the find method in order for the group method to be successful)
			if(!bookendMatcher.find()){ 
				return input;
			}
			
			String variable = bookendMatcher.group();			
			
			// Check for relative property reference
			if(!variable.contains(NAMESPACE_SEPARATOR)){
				variable = namespace + NAMESPACE_SEPARATOR + variable;
			}

			// Get the different components of the property name
			String[] variableParts = variable.split(NAMESPACE_SEPARATOR);
			String value = retrieveProperty(
				variableParts[NAMESPACE_POSITION],
				variableParts[SETTING_NAME_POSITION]
			);
			
			// Replace the variable with the substituted value
			result = result.replace(variablePlaceHolder, value);
			
			// Reset the matcher for the updated string
			placeholderMatcher.reset(result);
		}
				
		return result;
	}
	
	/**
	 * Load the specified properties file (relative to the base 
	 * configuration directory) into the specified namespace 
	 * (replacing the existing property set).
	 * 
	 * @param nameSpace name space to load the properties into
	 * @param filePath path and name of the properties file to load (either absolute or relative)
	 * @throws IOException problem reading configFile file
	 * @throws FileNotFoundException configFile file not found
	 */
	public void load(String nameSpace, String filePath) throws FileNotFoundException, IOException{
		Properties prop = new Properties(baseProps);
				
		//adjust the config file path to be relative to the config directory
		filePath = adjustPath(filePath);
		
		//load the properties from the file
		prop.load(new FileInputStream(filePath));
		
		//record the property set and its originating file
		properties.put(nameSpace, prop);
		files.put(nameSpace, filePath);		
	}
	
	/**
	 * Store the specified namespace back to disk for persistence purposes.
	 * This will be called for all properties files which have the 
	 * autoflush=true
	 * property set. All other property namespaces must be explicitly
	 * stored by calling this method.
	 * 
	 * @param nameSpace the namespace to store back to disk 
	 * @throws IOException problem writing configFile file
	 * @throws FileNotFoundException configFile file not found
	 */
	public void save(String nameSpace) throws FileNotFoundException, IOException{
		String filePath = files.get(nameSpace);
		Properties props = properties.get(nameSpace);
		
		props.store(new FileOutputStream(filePath), new Date().toString());
	}
	
	/**
	 * Store the specified namespace back to disk using the specified target
	 * file.
	 * 
	 * @param nameSpace the namespace which should be saved in a new location
	 * @param filePath the new path for the configFile file, relative or absolute
	 * @throws IOException problem writing configFile file
	 * @throws FileNotFoundException configFile file not found
	 */
	public void save(String nameSpace, String filePath) throws FileNotFoundException, IOException{
		files.put(nameSpace, filePath);
		save(nameSpace);
	}
	
	/**
	 * Refresh the specified namespace from disk in order to insure
	 * that the properties are not stale.
	 * 
	 * @param nameSpace the namespace to refresh from disk
	 * @throws IOException problem reading configFile file
	 * @throws FileNotFoundException configFile file not found
	 */
	public void refresh(String nameSpace) throws FileNotFoundException, IOException{
		String filePath = files.get(nameSpace);
		Properties updatedProps = new Properties();
		Properties oldProps = properties.get(nameSpace);
		
		//load the updated properties file, clear the old properties
		updatedProps.load(new FileInputStream(filePath));
		oldProps.clear();
		properties.put(nameSpace, updatedProps);
	}
	
	/**
	 * Close the properties and remove it from the settings instance.
	 * This will call the save method for this namespace only if the 
	 * autoflush=true
	 * property is set. All other namespaces will be closed normally
	 * and should be manually stored by calling the save method if
	 * persistence is desired.
	 * 
	 * @param nameSpace the namespace to close and purge from this 
	 * instance
	 * @throws IOException problem writing configFile file
	 * @throws FileNotFoundException configFile file not found
	 */
	public void close(String nameSpace) throws FileNotFoundException, IOException{
		//find out if we need to flush prior to closing
		boolean flushBeforeClose = getPropertyBoolean(
				nameSpace + NAMESPACE_SEPARATOR + SETTINGS_AUTOFLUSH);
		
		//flush to disk if necessary
		if(flushBeforeClose){
			save(nameSpace);
		}
		
		// remove the properties and file mapping
		properties.remove(nameSpace);
		files.remove(nameSpace);
	}
	
	/**
	 * Finalize the Settings object so that the namespace which should
	 * autoflush to disk prior to exit will do so. This method closes
	 * all of the open names spaces and automatically flushes the
	 * namespaces which request auto-flushing.
	 * 
	 * @param args the command line arguments passed to the application
	 * during invocation.
	 * @throws IOException if there is a problem writing to the configFile file
	 * @throws FileNotFoundException if the configFile file is not found
	 */
	void finalize(String[] args) throws FileNotFoundException, IOException{
		for(String namespace: getNamespaces()){
			close(namespace);			//close will auto-flush if necessary
		}
	}
	
	/**
	 * Adjust the config file path to be relative to the base config file as long as the
	 * file path is not already absolute
	 * 
	 * @param path a relative or absolute path to a config file
	 * @return the path to the config file, relative to the config directory if a relative path
	 * 		otherwise the absolute path
	 */
	protected String adjustPath(String path){
		if(new File(path).isAbsolute()){
			return path;
		} else {
			return baseDirectory + "/" + path;
		}
	}
}
